{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "81a6f12e",
   "metadata": {},
   "source": [
    "下面几段代码展示朴素贝叶斯模型的训练和预测。这里使用的数据集为本书自制的Books数据集，包含约1万本图书的标题，分为3种主题。首先是预处理，针对文本分类的预处理主要包含以下步骤：\n",
    "\n",
    "- 通常可以将英文文本全部转换为小写，或者将中文内容全部转换为简体，等等，这一般不会改变文本内容。\n",
    "- 去除标点。英文中的标点符号和单词之间没有空格（如——“Hi, there!”），如果不去除标点，“Hi,”和“there!”会被识别为不同于“Hi”和“there”的两个词，这显然是不合理的。对于中文，移除标点一般也不会影响文本的内容。\n",
    "- 分词。中文汉字之间没有空格分隔，中文分词有时比英文分词更加困难，此处不再赘述。\n",
    "- 去除停用词（如“I”、“is”、“的”等）。这些词往往大量出现但没有具体含义。\n",
    "- 建立词表。通常会忽略语料库中频率非常低的词。\n",
    "- 将词转换为词表索引（ID），便于机器学习模型使用。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "5936ceb0",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "train size = 8627 , test size = 2157\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|██████████████████████████████████████████████████████████████████████████████| 8627/8627 [11:26<00:00, 12.58it/s]\n",
      "100%|██████████████████████████████████████████████████████████████████████████████| 2157/2157 [00:35<00:00, 60.53it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['python', '编程', '入门', '教程']\n",
      "{'计算机类': 0, '艺术传媒类': 1, '经管类': 2}\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "import json\n",
    "import os\n",
    "import requests\n",
    "import re\n",
    "from tqdm import tqdm\n",
    "from collections import defaultdict\n",
    "from string import punctuation\n",
    "import spacy\n",
    "from spacy.lang.zh.stop_words import STOP_WORDS\n",
    "nlp = spacy.load('zh_core_web_sm')\n",
    "\n",
    "\n",
    "class BooksDataset:\n",
    "    def __init__(self):\n",
    "        train_file, test_file = 'train.jsonl', 'test.jsonl'\n",
    "\n",
    "        # 下载数据为JSON格式，转化为Python对象\n",
    "        def read_file(file_name):\n",
    "            with open(file_name, 'r', encoding='utf-8') as fin:\n",
    "                json_list = list(fin)\n",
    "            data_split = []\n",
    "            for json_str in json_list:\n",
    "                data_split.append(json.loads(json_str))\n",
    "            return data_split\n",
    "\n",
    "        self.train_data, self.test_data = read_file(train_file),\\\n",
    "            read_file(test_file)\n",
    "        print('train size =', len(self.train_data), \n",
    "              ', test size =', len(self.test_data))\n",
    "        \n",
    "        # 建立文本标签和数字标签的映射\n",
    "        self.label2id, self.id2label = {}, {}\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in data_split:\n",
    "                txt = data['class']\n",
    "                if txt not in self.label2id:\n",
    "                    idx = len(self.label2id)\n",
    "                    self.label2id[txt] = idx\n",
    "                    self.id2label[idx] = txt\n",
    "                label_id = self.label2id[txt]\n",
    "                data['label'] = label_id\n",
    "\n",
    "    def tokenize(self, attr='book'):\n",
    "        # 使用以下两行命令安装spacy用于中文分词\n",
    "        # pip install -U spacy\n",
    "        # python -m spacy download zh_core_web_sm\n",
    "        # 去除文本中的符号和停用词\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in tqdm(data_split):\n",
    "                # 转为小写\n",
    "                text = data[attr].lower()\n",
    "                # 符号替换为空\n",
    "                tokens = [t.text for t in nlp(text) \\\n",
    "                    if t.text not in STOP_WORDS]\n",
    "                # 这一步比较耗时，因此把tokenize的结果储存起来\n",
    "                data['tokens'] = tokens\n",
    "\n",
    "    # 根据分词结果建立词表，忽略部分低频词，\n",
    "    # 可以设置词最短长度和词表最大大小\n",
    "    def build_vocab(self, min_freq=3, min_len=2, max_size=None):\n",
    "        frequency = defaultdict(int)\n",
    "        for data in self.train_data:\n",
    "            tokens = data['tokens']\n",
    "            for token in tokens:\n",
    "                frequency[token] += 1 \n",
    "\n",
    "        print(f'unique tokens = {len(frequency)}, '+\\\n",
    "              f'total counts = {sum(frequency.values())}, '+\\\n",
    "              f'max freq = {max(frequency.values())}, '+\\\n",
    "              f'min freq = {min(frequency.values())}')    \n",
    "\n",
    "        self.token2id = {}\n",
    "        self.id2token = {}\n",
    "        total_count = 0\n",
    "        for token, freq in sorted(frequency.items(),\\\n",
    "            key=lambda x: -x[1]):\n",
    "            if max_size and len(self.token2id) >= max_size:\n",
    "                break\n",
    "            if freq > min_freq:\n",
    "                if (min_len is None) or (min_len and \\\n",
    "                    len(token) >= min_len):\n",
    "                    self.token2id[token] = len(self.token2id)\n",
    "                    self.id2token[len(self.id2token)] = token\n",
    "                    total_count += freq\n",
    "            else:\n",
    "                break\n",
    "        print(f'min_freq = {min_freq}, min_len = {min_len}, '+\\\n",
    "              f'max_size = {max_size}, '\n",
    "              f'remaining tokens = {len(self.token2id)}, '\n",
    "              f'in-vocab rate = {total_count / sum(frequency.values())}')\n",
    "\n",
    "    # 将分词后的结果转化为数字索引\n",
    "    def convert_tokens_to_ids(self):\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in data_split:\n",
    "                data['token_ids'] = []\n",
    "                for token in data['tokens']:\n",
    "                    if token in self.token2id:\n",
    "                        data['token_ids'].append(self.token2id[token])\n",
    "\n",
    "        \n",
    "dataset = BooksDataset()\n",
    "dataset.tokenize()\n",
    "print(dataset.train_data[0]['tokens'])\n",
    "print(dataset.label2id)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26d79e05",
   "metadata": {},
   "source": [
    "完成分词后，对出现次数超过3次的词元建立词表，并将分词后的文档转化为词元id的序列。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "0d6b1918",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "unique tokens = 6956, total counts = 54884, max freq = 1635, min freq = 1\n",
      "min_freq = 3, min_len = 2, max_size = None, remaining tokens = 1650, in-vocab rate = 0.7944209605713869\n",
      "[18, 26, 5, 0]\n"
     ]
    }
   ],
   "source": [
    "dataset.build_vocab(min_freq=3)\n",
    "dataset.convert_tokens_to_ids()\n",
    "print(dataset.train_data[0]['token_ids'])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d096d95f",
   "metadata": {},
   "source": [
    "接下来将数据和标签准备成便于训练的矩阵格式。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "ba632265",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "train_X, train_Y = [], []\n",
    "test_X, test_Y = [], []\n",
    "\n",
    "for data in dataset.train_data:\n",
    "    x = np.zeros(len(dataset.token2id), dtype=np.int32)\n",
    "    for token_id in data['token_ids']:\n",
    "        x[token_id] += 1\n",
    "    train_X.append(x)\n",
    "    train_Y.append(data['label'])\n",
    "for data in dataset.test_data:\n",
    "    x = np.zeros(len(dataset.token2id), dtype=np.int32)\n",
    "    for token_id in data['token_ids']:\n",
    "        x[token_id] += 1\n",
    "    test_X.append(x)\n",
    "    test_Y.append(data['label'])\n",
    "train_X, train_Y = np.array(train_X), np.array(train_Y)\n",
    "test_X, test_Y = np.array(test_X), np.array(test_Y)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3938acdb",
   "metadata": {},
   "source": [
    "下面代码展示朴素贝叶斯的训练和预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "f13251b7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "P(计算机类) = 0.4453460067230787\n",
      "P(艺术传媒类) = 0.26660484525327466\n",
      "P(经管类) = 0.2880491480236467\n",
      "P(教程|计算机类) = 0.5726495726495726\n",
      "P(基础|计算机类) = 0.6503006012024048\n",
      "P(设计|计算机类) = 0.606694560669456\n",
      "test example-0, prediction = 0, label = 0\n",
      "test example-1, prediction = 0, label = 0\n",
      "test example-2, prediction = 1, label = 1\n",
      "test example-3, prediction = 1, label = 1\n",
      "test example-4, prediction = 1, label = 1\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "\n",
    "class NaiveBayes:\n",
    "    def __init__(self, num_classes, vocab_size):\n",
    "        self.num_classes = num_classes\n",
    "        self.vocab_size = vocab_size\n",
    "        self.prior = np.zeros(num_classes, dtype=np.float64)\n",
    "        self.likelihood = np.zeros((num_classes, vocab_size),\\\n",
    "            dtype=np.float64)\n",
    "        \n",
    "    def fit(self, X, Y):\n",
    "        # NaiveBayes的训练主要涉及先验概率和似然的估计，\n",
    "        # 这两者都可以通过计数简单获得\n",
    "        for x, y in zip(X, Y):\n",
    "            self.prior[y] += 1\n",
    "            for token_id in x:\n",
    "                self.likelihood[y, token_id] += 1\n",
    "                \n",
    "        self.prior /= self.prior.sum()\n",
    "        # laplace平滑\n",
    "        self.likelihood += 1\n",
    "        self.likelihood /= self.likelihood.sum(axis=0)\n",
    "        # 为了避免精度溢出，使用对数概率\n",
    "        self.prior = np.log(self.prior)\n",
    "        self.likelihood = np.log(self.likelihood)\n",
    "    \n",
    "    def predict(self, X):\n",
    "        # 算出各个类别的先验概率与似然的乘积，找出最大的作为分类结果\n",
    "        preds = []\n",
    "        for x in X:\n",
    "            p = np.zeros(self.num_classes, dtype=np.float64)\n",
    "            for i in range(self.num_classes):\n",
    "                p[i] += self.prior[i]\n",
    "                for token in x:\n",
    "                    p[i] += self.likelihood[i, token]\n",
    "            preds.append(np.argmax(p))\n",
    "        return preds\n",
    "\n",
    "nb = NaiveBayes(len(dataset.label2id), len(dataset.token2id))\n",
    "train_X, train_Y = [], []\n",
    "for data in dataset.train_data:\n",
    "    train_X.append(data['token_ids'])\n",
    "    train_Y.append(data['label'])\n",
    "nb.fit(train_X, train_Y)\n",
    "\n",
    "for i in range(3):\n",
    "    print(f'P({dataset.id2label[i]}) = {np.exp(nb.prior[i])}')\n",
    "for i in range(3):\n",
    "    print(f'P({dataset.id2token[i]}|{dataset.id2label[0]}) = '+\\\n",
    "          f'{np.exp(nb.likelihood[0, i])}')\n",
    "\n",
    "test_X, test_Y = [], []\n",
    "for data in dataset.test_data:\n",
    "    test_X.append(data['token_ids'])\n",
    "    test_Y.append(data['label'])\n",
    "    \n",
    "NB_preds = nb.predict(test_X)\n",
    "    \n",
    "for i, (p, y) in enumerate(zip(NB_preds, test_Y)):\n",
    "    if i >= 5:\n",
    "        break\n",
    "    print(f'test example-{i}, prediction = {p}, label = {y}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1cf6399",
   "metadata": {},
   "source": [
    "下面使用第3章介绍的TF-IDF方法得到文档的特征向量，并使用PyTorch实现逻辑斯谛回归模型的训练和预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "3fdaeb8a-1f81-4445-a0d6-4bdd6079f327",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Defaulting to user installation because normal site-packages is not writeableNote: you may need to restart the kernel to use updated packages.\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "WARNING: Skipping C:\\ProgramData\\anaconda3\\Lib\\site-packages\\python_lsp_server-1.7.2.dist-info due to invalid metadata entry 'name'\n",
      "WARNING: Skipping C:\\ProgramData\\anaconda3\\Lib\\site-packages\\python_lsp_server-1.7.2.dist-info due to invalid metadata entry 'name'\n",
      "WARNING: Skipping C:\\ProgramData\\anaconda3\\Lib\\site-packages\\python_lsp_server-1.7.2.dist-info due to invalid metadata entry 'name'\n",
      "WARNING: Skipping C:\\ProgramData\\anaconda3\\Lib\\site-packages\\python_lsp_server-1.7.2.dist-info due to invalid metadata entry 'name'\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Requirement already satisfied: utils in e:\\translations\\python\\python311\\site-packages (1.0.2)\n"
     ]
    }
   ],
   "source": [
    "pip install utils"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "21a3bc79",
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import sys\n",
    "\n",
    "sys.path.append('../code')\n",
    "from my_utils import TFIDF\n",
    "        \n",
    "tfidf = TFIDF(len(dataset.token2id))\n",
    "tfidf.fit(train_X)\n",
    "train_F = tfidf.transform(train_X)\n",
    "test_F = tfidf.transform(test_X)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dc8af30b",
   "metadata": {},
   "source": [
    "逻辑斯谛回归可以看作一个一层的神经网络模型，使用PyTorch实现可以方便地利用自动求导功能。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "1ddebf0c",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "epoch-49, loss=0.2532: 100%|█| 50/50 [00:24<00:00,  2.05it/s\n"
     ]
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "eval_loss = 0.2868\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "from torch import nn\n",
    "\n",
    "class LR(nn.Module):\n",
    "    def __init__(self, input_dim, output_dim):\n",
    "        super(LR, self).__init__()\n",
    "        self.linear = nn.Linear(input_dim, output_dim)\n",
    "        \n",
    "    def forward(self, input_feats, labels=None):\n",
    "        outputs = self.linear(input_feats)\n",
    "        \n",
    "        if labels is not None:\n",
    "            loss_fc = nn.CrossEntropyLoss()\n",
    "            loss = loss_fc(outputs, labels)\n",
    "            return (loss, outputs)\n",
    "        \n",
    "        return outputs\n",
    "\n",
    "model = LR(len(dataset.token2id), len(dataset.label2id))\n",
    "\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "from torch.optim import SGD, Adam\n",
    "\n",
    "# 使用PyTorch的DataLoader来进行数据循环，因此按照PyTorch的接口\n",
    "# 实现myDataset和DataCollator两个类\n",
    "# myDataset是对特征向量和标签的简单封装便于对齐接口，\n",
    "# DataCollator用于批量将数据转化为PyTorch支持的张量类型\n",
    "class myDataset(Dataset):\n",
    "    def __init__(self, X, Y):\n",
    "        self.X = X\n",
    "        self.Y = Y\n",
    "        \n",
    "    def __len__(self):\n",
    "        return len(self.X)\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        return (self.X[idx], self.Y[idx])\n",
    "\n",
    "class DataCollator:\n",
    "    @classmethod\n",
    "    def collate_batch(cls, batch):\n",
    "        feats, labels = [], []\n",
    "        for x, y in batch:\n",
    "            feats.append(x)\n",
    "            labels.append(y)\n",
    "        # 直接将一个ndarray的列表转化为张量是非常慢的，\n",
    "        # 所以需要提前将列表转化为一整个ndarray\n",
    "        feats = torch.tensor(np.array(feats), dtype=torch.float)\n",
    "        labels = torch.tensor(np.array(labels), dtype=torch.long)\n",
    "        return {'input_feats': feats, 'labels': labels}\n",
    "\n",
    "# 设置训练超参数和优化器，模型初始化\n",
    "epochs = 50\n",
    "batch_size = 128\n",
    "learning_rate = 1e-3\n",
    "weight_decay = 0\n",
    "\n",
    "train_dataset = myDataset(train_F, train_Y)\n",
    "test_dataset = myDataset(test_F, test_Y)\n",
    "\n",
    "data_collator = DataCollator()\n",
    "train_dataloader = DataLoader(train_dataset, batch_size=batch_size,\\\n",
    "    shuffle=True, collate_fn=data_collator.collate_batch)\n",
    "test_dataloader = DataLoader(test_dataset, batch_size=batch_size,\\\n",
    "    shuffle=False, collate_fn=data_collator.collate_batch)\n",
    "optimizer = Adam(model.parameters(), lr=learning_rate,\\\n",
    "    weight_decay=weight_decay)\n",
    "model.zero_grad()\n",
    "model.train()\n",
    "\n",
    "from tqdm import tqdm, trange\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# 模型训练\n",
    "with trange(epochs, desc='epoch', ncols=60) as pbar:\n",
    "    epoch_loss = []\n",
    "    for epoch in pbar:\n",
    "        model.train()\n",
    "        for step, batch in enumerate(train_dataloader):\n",
    "            loss = model(**batch)[0]\n",
    "            pbar.set_description(f'epoch-{epoch}, loss={loss.item():.4f}')\n",
    "            loss.backward()\n",
    "            optimizer.step()\n",
    "            model.zero_grad()\n",
    "            epoch_loss.append(loss.item())\n",
    "\n",
    "    epoch_loss = np.array(epoch_loss)\n",
    "    # 打印损失曲线\n",
    "    plt.plot(range(len(epoch_loss)), epoch_loss)\n",
    "    plt.xlabel('training epoch')\n",
    "    plt.ylabel('loss')\n",
    "    plt.show()\n",
    "    \n",
    "    model.eval()\n",
    "    with torch.no_grad():\n",
    "        loss_terms = []\n",
    "        for batch in test_dataloader:\n",
    "            loss = model(**batch)[0]\n",
    "            loss_terms.append(loss.item())\n",
    "        print(f'eval_loss = {np.mean(loss_terms):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10808854",
   "metadata": {},
   "source": [
    "下面的代码使用训练好的模型对测试集进行预测，并报告分类结果。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "11a9bf62",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "test example-0, prediction = 0, label = 0\n",
      "test example-1, prediction = 0, label = 0\n",
      "test example-2, prediction = 1, label = 1\n",
      "test example-3, prediction = 1, label = 1\n",
      "test example-4, prediction = 1, label = 1\n"
     ]
    }
   ],
   "source": [
    "LR_preds = []\n",
    "model.eval()\n",
    "for batch in test_dataloader:\n",
    "    with torch.no_grad():\n",
    "        _, preds = model(**batch)\n",
    "        preds = np.argmax(preds, axis=1)\n",
    "        LR_preds.extend(preds)\n",
    "            \n",
    "for i, (p, y) in enumerate(zip(LR_preds, test_Y)):\n",
    "    if i >= 5:\n",
    "        break\n",
    "    print(f'test example-{i}, prediction = {p}, label = {y}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c5feb65e",
   "metadata": {},
   "source": [
    "下面的代码展示多分类情况下宏平均和微平均的算法。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "a5ac32c5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "NB: micro-f1 = 0.8961520630505331, macro-f1 = 0.8948572078813896\n",
      "LR: micro-f1 = 0.9151599443671766, macro-f1 = 0.9144981847794594\n"
     ]
    }
   ],
   "source": [
    "test_Y = np.array(test_Y)\n",
    "NB_preds = np.array(NB_preds)\n",
    "LR_preds = np.array(LR_preds)\n",
    "\n",
    "def micro_f1(preds, labels):\n",
    "    TP = np.sum(preds == labels)\n",
    "    FN = FP = 0\n",
    "    for i in range(len(dataset.label2id)):\n",
    "        FN += np.sum((preds == i) & (labels != i))\n",
    "        FP += np.sum((preds != i) & (labels == i))\n",
    "    precision = TP / (TP + FP)\n",
    "    recall = TP / (TP + FN)\n",
    "    f1 = 2 * precision * recall / (precision + recall)\n",
    "    return f1\n",
    "\n",
    "def macro_f1(preds, labels):\n",
    "    f_scores = []\n",
    "    for i in range(len(dataset.label2id)):\n",
    "        TP = np.sum((preds == i) & (labels == i))\n",
    "        FN = np.sum((preds == i) & (labels != i))\n",
    "        FP = np.sum((preds != i) & (labels == i))\n",
    "        precision = TP / (TP + FP)\n",
    "        recall = TP / (TP + FN)\n",
    "        f1 = 2 * precision * recall / (precision + recall)\n",
    "        f_scores.append(f1)\n",
    "    return np.mean(f_scores)\n",
    "\n",
    "print(f'NB: micro-f1 = {micro_f1(NB_preds, test_Y)}, '+\\\n",
    "      f'macro-f1 = {macro_f1(NB_preds, test_Y)}')\n",
    "print(f'LR: micro-f1 = {micro_f1(LR_preds, test_Y)}, '+\\\n",
    "      f'macro-f1 = {macro_f1(LR_preds, test_Y)}')"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
